如何实现简单的请求鉴权

如何利用对称加密实现简单的请求鉴权。

前期沟通

服务端与客户端需要在前期敲定以下内容:

  1. 秘钥对(apiKey和secretKey),由服务端通过安全的途径交给客户端,如邮件、IM等内部渠道。
  2. 头部名称,包括APIKey、时间戳、签名及业务相关的头部。
  3. 加签算法,即根据业务参数及secretKey如何生成加密签名,客户端与服务端需保持一致。由客户端加密后的内容,在服务端用同样的秘钥加密应该是一模一样的。

服务端

验签流程

大致流程如下图所示。
img

代码

通过Interceptor来做拦截,并根据验签结果来决定对请求是否放行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public class UserInterceptor implements HandlerInterceptor {

private final static String AUTH_HEADER_APIKEY = "X-Header-APIKey";
private final static String AUTH_HEADER_TIMESTAMP = "X-Header-Timestamp";
private final static String AUTH_HEADER_SIGNATURE= "X-Header-Signature";
private final static String AUTH_HEADER_USERID = "X-Header-UserID";

private static final Logger logger = LoggerFactory.getLogger(UserInterceptor.class);
@Override
public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return checkSystemAuth(request, response);
}

private boolean checkSystemAuth(HttpServletRequest request, HttpServletResponse response) {
// 1. 检查头部完整
String reqApiKey = request.getHeader(AUTH_HEADER_APIKEY);
String reqTimestamp = request.getHeader(AUTH_HEADER_TIMESTAMP);
String reqSign = request.getHeader(AUTH_HEADER_SIGNATURE);
String userId = request.getHeader(AUTH_HEADER_USERID);
if(StringUtils.isEmpty(reqApiKey) || StringUtils.isEmpty(reqTimestamp) || StringUtils.isEmpty(reqSign) || StringUtils.isEmpty(userId)) {
logger.error("missing apikey or timestamp or signature or userid header");
return false;
}
// 2. 检查timestamp超时
if(!isInTime(reqTimestamp)) {
logger.error("timestamp header timedout");
return false;
}
// 3. 根据apikey,从DB中找到对应的secretkey,keypairMapper为DAO对象
KeyPair keyStore = keypairMapper.getOneByApiKey(reqApiKey);
if(null == keyStore) {
logger.error("cannot find secretkey from apikey");
return false;
}
String secretKey = keyStore.getSecretKey();
// 4. 将除签名外的头部生成有序map
SortedMap<String, String> reqForm = new TreeMap<>();
reqForm.put(AUTH_HEADER_APIKEY, reqApiKey);
reqForm.put(AUTH_HEADER_TIMESTAMP, reqTimestamp);
reqForm.put(AUTH_HEADER_USERID, userId);
// 5. 计算出签名并与传来的签名比对
String calculatedSign = sign(reqForm, secretKey);
if(!reqSign.equals(calculatedSign)) {
logger.error("mismatched signatures");
return false;
}
logger.debug("system auth passed");
return true;
}

private boolean isInTime(String timeStr) {
try {
long time = Long.parseLong(timeStr);
if (System.currentTimeMillis() - time <= interceptorProperties.getDefaultTimestampTimeout()) {
return true;
} else {
logger.error("Timestamp in request timed out.");
return false;
}
} catch (NumberFormatException e) {
logger.error("Invalid timestamp: {}", e.getMessage());
return false;
}
}

private String sign(SortedMap<String, String> reqForm, String secretKey) {
try {
// 1. 将有序map组合成url串
List<String> kvList = new ArrayList<>();
for (Map.Entry<String, String> paramEntry : reqForm.entrySet()) {
kvList.add(paramEntry.getKey() + "=" + URLEncoder.encode(
StringUtils.isEmpty(
paramEntry.getValue()) ? "" : paramEntry.getValue(), Charsets.UTF_8.name()
)
);
}
// 2. 计算签名
String queryString = StringUtils.join(kvList, '&').toLowerCase();
String signature = Base64.encodeBase64String(new HmacUtils(HmacAlgorithms.HMAC_SHA_1, secretKey).hmac(queryString));
// 3. 二次encode
String encodedSign = URLEncoder.encode(signature, Charsets.UTF_8.name());
return encodedSign;
} catch (Exception e) {
logger.error("Signature error: {}", e.getMessage());
return null;
}
}
}

客户端

流程

客户端的加签过程如下图所示。
img

代码

Java版的客户端代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class AuthTest {
private final static String AUTH_HEADER_APIKEY = "X-Header-APIKey";
private final static String AUTH_HEADER_TIMESTAMP = "X-Header-Timestamp";
private final static String AUTH_HEADER_SIGNATURE= "X-Header-Signature";
private final static String AUTH_HEADER_USERID = "X-Header-UserID";

public static void main(String[] args) {
String url;
...// 构造server url
// apikey及secretkey,由服务端提供并由客户端保存
String apiKey = "xxx";
String secretKey = "yyy";

RestTemplate restTemplate = new RestTemplate();
Long timestamp = System.currentTimeMillis();
// 1. 构造模拟请求参数列
String sortedHeaders = new StringBuilder("?")
.append(AUTH_HEADER_APIKEY)
.append("=")
.append(apiKey)
.append("&")
.append(AUTH_HEADER_TIMESTAMP)
.append("=")
.append(timestamp)
.append("&")
.append(AUTH_HEADER_USERID)
.append("=luckliu").toString();
SortedMap<String, String> paramMap = extractFromUrlParamToMap(sortedHeaders);
// 2. 计算签名
String signature = sign(paramMap, secretKey);
// 3. 放置头部
HttpHeaders headers = new HttpHeaders();
headers.set(AUTH_HEADER_APIKEY, apiKey);
headers.set(AUTH_HEADER_TIMESTAMP, Long.toString(timestamp));
headers.set(AUTH_HEADER_SIGNATURE, signature);
headers.set(AUTH_HEADER_USERID, "luckliu");
String body = "!dlrow olleH";
HttpEntity<String> request = new HttpEntity<String>(body, headers);
// 4. 发起请求
ResponseEntity<Void> responseEntity = restTemplate.postForEntity(url, request, Void.class);
}

/**
* 截取url问号后面的参数, 并转换成SortedMap
* @param url
* @return
*/

private static SortedMap<String, String> extractFromUrlParamToMap(String url) {
// TODO 需考虑参数为空等异常情况
String[] paramArr = url.substring(url.indexOf("?")+1).split("&");
SortedMap<String, String> paramMap = Maps.newTreeMap();
Arrays.stream(paramArr).forEach(
p->paramMap.put(p.substring(0,p.indexOf("=")), p.substring(p.indexOf("=")+1))
);
return paramMap;
}

// 此处的sign方法应与服务端的保持一致
private static String sign(SortedMap<String, String> reqForm, String secretKey) {
try {
// 组合成url串
List<String> kvList = new ArrayList<>();
for (Map.Entry<String, String> paramEntry : reqForm.entrySet()) {
kvList.add(paramEntry.getKey() + "=" + URLEncoder.encode(
StringUtils.isEmpty(
paramEntry.getValue()) ? "" : paramEntry.getValue(), Charsets.UTF_8.name()
)
);
}
String queryString = StringUtils.join(kvList, '&').toLowerCase();
// 计算签名
String signature = Base64.encodeBase64String(new HmacUtils(HmacAlgorithms.HMAC_SHA_1, secretKey).hmac(queryString));
// 二次encode
String encodedSign = URLEncoder.encode(signature, Charsets.UTF_8.name());
return encodedSign;
} catch (Exception e) {
return null;
}
}
}

再来一个golang版本的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)

// 加签算法
func Hmac(key, data []byte) string {
mac := hmac.New(sha1.New, key)
mac.Write(data)
return url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum([]byte(""))))
}
func main() {
...// 构造server url
body := "!dlroW olleH"
// 1. 通过设置系统校验头部来调用接口
apikey := "xxx"
secretKey := "yyy"
timestamp := time.Now().UnixNano() / 1000000
// 2. 省略排序等步骤,将校验参数组织成有序的请求列
sortedHeaders := []byte(strings.ToLower("X-Header-APIKey=" + apikey + "&X-Header-Timestamp=" + strconv.FormatInt(timestamp, 10) + "&MLSS-DI-UserID=luckliu"))
// 3. 计算签名
signature := Hmac([]byte(secretKey), sortedHeaders)
fmt.Println("final sorted headers: ", string(sortedHeaders))
fmt.Println("calculated signature: " + signature)
// 4. 放置请求头部并发起请求
client := &http.Client{}
request, _ := http.NewRequest("POST", url, strings.NewReader(body))
request.Header.Set("X-Header-APIKey", apikey)
request.Header.Set("X-Header-Timestamp", strconv.FormatInt(timestamp, 10))
request.Header.Set("X-Header-Signature", signature)
request.Header.Set("X-Header-UserID", "luckliu")
response, _ := client.Do(request)
fmt.Println("response status: " + response.Status)
defer response.Body.Close()
}